/*
 * $Id: SwingXUtilities.java 3886 2010-11-16 16:28:58Z kschaefe $
 *
 * Copyright 2008 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
package org.jdesktop.swingx;

import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Window;
import java.awt.event.InputEvent;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.io.StringReader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Locale;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JList;
import javax.swing.JPopupMenu;
import javax.swing.KeyStroke;
import javax.swing.ListCellRenderer;
import javax.swing.ListModel;
import javax.swing.ListSelectionModel;
import javax.swing.MenuElement;
import javax.swing.RepaintManager;
import javax.swing.SwingUtilities;
import javax.swing.plaf.ComponentInputMapUIResource;
import javax.swing.plaf.UIResource;
import javax.swing.text.html.HTMLDocument;

import org.jdesktop.swingx.painter.Painter;
import org.jdesktop.swingx.plaf.PainterUIResource;

/**
 * A collection of utility methods for Swing(X) classes.
 * 
 * <ul>
 * PENDING JW: think about location of this class and/or its methods, Options:
 * 
 * <li>move this class to the swingx utils package which already has a bunch of
 * xxUtils
 * <li>move methods between xxUtils classes as appropriate (one window/comp
 * related util)
 * <li>keep here in swingx (consistent with swingutilities in core)
 * </ul>
 * 
 * @author Karl George Schaefer
 */
public final class SwingXUtilities {
	private SwingXUtilities() {
		// does nothing
	}

	/**
	 * A helper for creating and updating key bindings for components with
	 * mnemonics. The {@code pressed} action will be invoked when the mnemonic
	 * is activated.
	 * <p>
	 * TODO establish an interface for the mnemonic properties, such as
	 * {@code MnemonicEnabled} and change signature to
	 * {@code public static <T extends
	 * JComponent & MnemonicEnabled> void updateMnemonicBinding(T c, String
	 * pressed)}
	 * 
	 * @param c
	 *            the component bindings to update
	 * @param pressed
	 *            the name of the action in the action map to invoke when the
	 *            mnemonic is pressed
	 * @throws NullPointerException
	 *             if the component is {@code null}
	 */
	public static void updateMnemonicBinding(JComponent c, String pressed) {
		updateMnemonicBinding(c, pressed, null);
	}

	/**
	 * A helper for creating and updating key bindings for components with
	 * mnemonics. The {@code pressed} action will be invoked when the mnemonic
	 * is activated and the {@code released} action will be invoked when the
	 * mnemonic is deactivated.
	 * <p>
	 * TODO establish an interface for the mnemonic properties, such as
	 * {@code MnemonicEnabled} and change signature to
	 * {@code public static <T extends
	 * JComponent & MnemonicEnabled> void updateMnemonicBinding(T c, String
	 * pressed, String released)}
	 * 
	 * @param c
	 *            the component bindings to update
	 * @param pressed
	 *            the name of the action in the action map to invoke when the
	 *            mnemonic is pressed
	 * @param released
	 *            the name of the action in the action map to invoke when the
	 *            mnemonic is released (if the action is a toggle style, then
	 *            this parameter should be {@code null})
	 * @throws NullPointerException
	 *             if the component is {@code null}
	 */
	public static void updateMnemonicBinding(JComponent c, String pressed, String released) {
		Class<?> clazz = c.getClass();
		int m = -1;

		try {
			Method mtd = clazz.getMethod("getMnemonic");
			m = (Integer) mtd.invoke(c);
		} catch (RuntimeException e) {
			throw e;
		} catch (Exception e) {
			throw new IllegalArgumentException("unable to access mnemonic", e);
		}

		InputMap map = SwingUtilities.getUIInputMap(c, JComponent.WHEN_IN_FOCUSED_WINDOW);

		if (m != 0) {
			if (map == null) {
				map = new ComponentInputMapUIResource(c);
				SwingUtilities.replaceUIInputMap(c, JComponent.WHEN_IN_FOCUSED_WINDOW, map);
			}

			map.clear();

			// TODO is ALT_MASK right for all platforms?
			map.put(KeyStroke.getKeyStroke(m, InputEvent.ALT_MASK, false), pressed);
			map.put(KeyStroke.getKeyStroke(m, InputEvent.ALT_MASK, true), released);
			map.put(KeyStroke.getKeyStroke(m, 0, true), released);
		} else {
			if (map != null) {
				map.clear();
			}
		}
	}

	static <C extends JComponent & BackgroundPaintable> void installBackground(C comp, Color color) {
		if (isUIInstallable(color)) {
			// only handle UIResource, if null then painter isn't painted; this
			// allows optimized code paths
			if (comp.getBackgroundPainter() instanceof UIResource) {
				comp.setBackgroundPainter(new PainterUIResource<JComponent>(new BackgroundPainter(color)));
			}
			// does nothing otherwise; do not install UIResource Color over a
			// non-UIResource Painter
		} else {
			comp.setBackgroundPainter(new BackgroundPainter(color));
		}
	}

	@SuppressWarnings("unchecked")
	static <C extends JComponent & BackgroundPaintable> void paintBackground(C comp, Graphics2D g) {
		Painter<? super C> painter = comp.getBackgroundPainter();

		if (painter instanceof BackgroundPainter) {
			// ignore paintBorderInsets for BackgroundPainter
			painter.paint(g, comp, comp.getWidth(), comp.getHeight());
		} else if (painter != null) {
			if (comp.isPaintBorderInsets()) {
				painter.paint(g, comp, comp.getWidth(), comp.getHeight());
			} else {
				Insets insets = comp.getInsets();
				g.translate(insets.left, insets.top);
				painter.paint(g, comp, comp.getWidth() - insets.left - insets.right, comp.getHeight() - insets.top
						- insets.bottom);
				g.translate(-insets.left, -insets.top);
			}
		}
	}

	private static Component[] getChildren(Component c) {
		Component[] children = null;

		if (c instanceof MenuElement) {
			MenuElement[] elements = ((MenuElement) c).getSubElements();
			children = new Component[elements.length];

			for (int i = 0; i < elements.length; i++) {
				children[i] = elements[i].getComponent();
			}
		} else if (c instanceof Container) {
			children = ((Container) c).getComponents();
		}

		return children;
	}

	/**
	 * Enables or disables of the components in the tree starting with {@code c}
	 * .
	 * 
	 * @param c
	 *            the starting component
	 * @param enabled
	 *            {@code true} if the component is to enabled; {@code false}
	 *            otherwise
	 */
	public static void setComponentTreeEnabled(Component c, boolean enabled) {
		c.setEnabled(enabled);

		Component[] children = getChildren(c);

		if (children != null) {
			for (int i = 0; i < children.length; i++) {
				setComponentTreeEnabled(children[i], enabled);
			}
		}
	}

	/**
	 * Sets the locale for an entire component hierarchy to the specified
	 * locale.
	 * 
	 * @param c
	 *            the starting component
	 * @param locale
	 *            the locale to set
	 */
	public static void setComponentTreeLocale(Component c, Locale locale) {
		c.setLocale(locale);

		Component[] children = getChildren(c);

		if (children != null) {
			for (int i = 0; i < children.length; i++) {
				setComponentTreeLocale(children[i], locale);
			}
		}
	}

	/**
	 * Sets the background for an entire component hierarchy to the specified
	 * color.
	 * 
	 * @param c
	 *            the starting component
	 * @param color
	 *            the color to set
	 */
	public static void setComponentTreeBackground(Component c, Color color) {
		c.setBackground(color);

		Component[] children = getChildren(c);

		if (children != null) {
			for (int i = 0; i < children.length; i++) {
				setComponentTreeBackground(children[i], color);
			}
		}
	}

	/**
	 * Sets the foreground for an entire component hierarchy to the specified
	 * color.
	 * 
	 * @param c
	 *            the starting component
	 * @param color
	 *            the color to set
	 */
	public static void setComponentTreeForeground(Component c, Color color) {
		c.setForeground(color);

		Component[] children = getChildren(c);

		if (children != null) {
			for (int i = 0; i < children.length; i++) {
				setComponentTreeForeground(children[i], color);
			}
		}
	}

	/**
	 * Sets the font for an entire component hierarchy to the specified font.
	 * 
	 * @param c
	 *            the starting component
	 * @param font
	 *            the font to set
	 */
	public static void setComponentTreeFont(Component c, Font font) {
		c.setFont(font);

		Component[] children = getChildren(c);

		if (children != null) {
			for (int i = 0; i < children.length; i++) {
				setComponentTreeFont(children[i], font);
			}
		}
	}

	private static String STYLESHEET = "body { margin-top: 0; margin-bottom: 0; margin-left: 0; margin-right: 0;"
			+ " font-family: %s; font-size: %dpt;  }" + "a, p, li { margin-top: 0; margin-bottom: 0; margin-left: 0;"
			+ " margin-right: 0; font-family: %s; font-size: %dpt;  }";

	/**
	 * Sets the font used for HTML displays to the specified font. Components
	 * that display HTML do not necessarily honor font properties, since the
	 * HTML document can override these values. Calling {@code setHtmlFont}
	 * after the data is set will force the HTML display to use the font
	 * specified to this method.
	 * 
	 * @param doc
	 *            the HTML document to update
	 * @param font
	 *            the font to use
	 * @throws NullPointerException
	 *             if any parameter is {@code null}
	 */
	public static void setHtmlFont(HTMLDocument doc, Font font) {
		String stylesheet = String.format(STYLESHEET, font.getName(), font.getSize(), font.getName(), font.getSize());

		try {
			doc.getStyleSheet().loadRules(new StringReader(stylesheet), null);
		} catch (IOException e) {
			// this should never happen with our sheet
			throw new IllegalStateException(e);
		}
	}

	/**
	 * Updates the componentTreeUI of all top-level windows of the current
	 * application.
	 * 
	 */
	public static void updateAllComponentTreeUIs() {
		// for (Frame frame : Frame.getFrames()) {
		// updateAllComponentTreeUIs(frame);
		// }
		// JW: updated to new 1.6 api - returns all windows, owned and ownerless
		for (Window window : Window.getWindows()) {
			SwingUtilities.updateComponentTreeUI(window);
		}
	}

	/**
	 * Updates the componentTreeUI of the given window and all its owned
	 * windows, recursively.
	 * 
	 * 
	 * @param window
	 *            the window to update
	 */
	public static void updateAllComponentTreeUIs(Window window) {
		SwingUtilities.updateComponentTreeUI(window);
		for (Window owned : window.getOwnedWindows()) {
			updateAllComponentTreeUIs(owned);
		}
	}

	/**
	 * A version of {@link SwingUtilities#invokeLater(Runnable)} that supports
	 * return values.
	 * 
	 * @param <T>
	 *            the return type of the callable
	 * @param callable
	 *            the callable to execute
	 * @return a future task for accessing the return value
	 * @see Callable
	 */
	public static <T> FutureTask<T> invokeLater(Callable<T> callable) {
		FutureTask<T> task = new FutureTask<T>(callable);

		SwingUtilities.invokeLater(task);

		return task;
	}

	/**
	 * A version of {@link SwingUtilities#invokeAndWait(Runnable)} that supports
	 * return values.
	 * 
	 * @param <T>
	 *            the return type of the callable
	 * @param callable
	 *            the callable to execute
	 * @return the value returned by the callable
	 * @throws InterruptedException
	 *             if we're interrupted while waiting for the event dispatching
	 *             thread to finish executing {@code callable.call()}
	 * @throws InvocationTargetException
	 *             if an exception is thrown while running {@code callable}
	 * @see Callable
	 */
	public static <T> T invokeAndWait(Callable<T> callable) throws InterruptedException, InvocationTargetException {
		try {
			// blocks until future returns
			return invokeLater(callable).get();
		} catch (ExecutionException e) {
			Throwable t = e.getCause();

			if (t instanceof RuntimeException) {
				throw (RuntimeException) t;
			} else if (t instanceof InvocationTargetException) {
				throw (InvocationTargetException) t;
			} else {
				throw new InvocationTargetException(t);
			}
		}
	}

	/**
	 * An improved version of
	 * {@link SwingUtilities#getAncestorOfClass(Class, Component)}. This method
	 * traverses {@code JPopupMenu} invoker and uses generics to return an
	 * appropriately typed object.
	 * 
	 * @param <T>
	 *            the type of ancestor to find
	 * @param clazz
	 *            the class instance of the ancestor to find
	 * @param c
	 *            the component to start the search from
	 * @return an ancestor of the correct type or {@code null} if no such
	 *         ancestor exists. This method also returns {@code null} if any
	 *         parameter is {@code null}.
	 */
	@SuppressWarnings("unchecked")
	public static <T> T getAncestor(Class<T> clazz, Component c) {
		if (clazz == null || c == null) {
			return null;
		}

		Component parent = c.getParent();

		while (parent != null && !(clazz.isInstance(parent))) {
			parent = parent instanceof JPopupMenu ? ((JPopupMenu) parent).getInvoker() : parent.getParent();
		}

		return (T) parent;
	}

	/**
	 * Returns whether the component is part of the parent's container
	 * hierarchy. If a parent in the chain is of type JPopupMenu, the parent
	 * chain of its invoker is walked.
	 * 
	 * @param focusOwner
	 * @param parent
	 * @return true if the component is contained under the parent's hierarchy,
	 *         coping with JPopupMenus.
	 */
	public static boolean isDescendingFrom(Component focusOwner, Component parent) {
		while (focusOwner != null) {
			if (focusOwner instanceof JPopupMenu) {
				focusOwner = ((JPopupMenu) focusOwner).getInvoker();
				if (focusOwner == null) {
					return false;
				}
			}
			if (focusOwner == parent) {
				return true;
			}
			focusOwner = focusOwner.getParent();
		}
		return false;
	}

	/**
	 * Obtains a {@code TranslucentRepaintManager} from the specified manager.
	 * If the current manager is a {@code TranslucentRepaintManager} or a
	 * {@code ForwardingRepaintManager} that contains a
	 * {@code TranslucentRepaintManager}, then the passed in manager is
	 * returned. Otherwise a new repaint manager is created and returned.
	 * 
	 * @param delegate
	 *            the current repaint manager
	 * @return a non-{@code null} {@code TranslucentRepaintManager}
	 * @throws NullPointerException
	 *             if {@code delegate} is {@code null}
	 */
	static RepaintManager getTranslucentRepaintManager(RepaintManager delegate) {
		RepaintManager manager = delegate;

		while (manager != null && !manager.getClass().isAnnotationPresent(TranslucentRepaintManager.class)) {
			if (manager instanceof ForwardingRepaintManager) {
				manager = ((ForwardingRepaintManager) manager).getDelegateManager();
			} else {
				manager = null;
			}
		}

		return manager == null ? new RepaintManagerX(delegate) : delegate;
	}

	/**
	 * Checks and returns whether the given property should be replaced by the
	 * UI's default value.
	 * 
	 * @param property
	 *            the property to check.
	 * @return true if the given property should be replaced by the UI's default
	 *         value, false otherwise.
	 */
	public static boolean isUIInstallable(Object property) {
		return (property == null) || (property instanceof UIResource);
	}

	// ---- methods c&p'ed from SwingUtilities2 to reduce dependencies on sun
	// packages

	/**
	 * Updates lead and anchor selection index without changing the selection.
	 * 
	 * Note: this is c&p'ed from SwingUtilities2 to not have any direct
	 * dependency.
	 * 
	 * @param selectionModel
	 *            the selection model to change lead/anchor
	 * @param lead
	 *            the lead selection index
	 * @param anchor
	 *            the anchor selection index
	 */
	public static void setLeadAnchorWithoutSelection(ListSelectionModel selectionModel, int lead, int anchor) {
		if (anchor == -1) {
			anchor = lead;
		}
		if (lead == -1) {
			selectionModel.setAnchorSelectionIndex(-1);
			selectionModel.setLeadSelectionIndex(-1);
		} else {
			if (selectionModel.isSelectedIndex(lead))
				selectionModel.addSelectionInterval(lead, lead);
			else {
				selectionModel.removeSelectionInterval(lead, lead);
			}
			selectionModel.setAnchorSelectionIndex(anchor);
		}
	}

	public static boolean shouldIgnore(MouseEvent mouseEvent, JComponent component) {
		return ((component == null) || (!(component.isEnabled())) || (!(SwingUtilities.isLeftMouseButton(mouseEvent))) || (mouseEvent
				.isConsumed()));
	}

	public static int loc2IndexFileList(JList list, Point point) {
		int i = list.locationToIndex(point);
		if (i != -1) {
			Object localObject = list.getClientProperty("List.isFileList");
			if ((localObject instanceof Boolean) && (((Boolean) localObject).booleanValue())
			// PENDING JW: this isn't aware of sorting/filtering - fix!
					&& (!(pointIsInActualBounds(list, i, point)))) {
				i = -1;
			}
		}
		return i;
	}

	// PENDING JW: this isn't aware of sorting/filtering - fix!
	private static boolean pointIsInActualBounds(JList list, int index, Point point) {
		ListCellRenderer renderer = list.getCellRenderer();
		ListModel model = list.getModel();
		Object element = model.getElementAt(index);
		Component comp = renderer.getListCellRendererComponent(list, element, index, false, false);

		Dimension prefSize = comp.getPreferredSize();
		Rectangle cellBounds = list.getCellBounds(index, index);
		if (!(comp.getComponentOrientation().isLeftToRight())) {
			cellBounds.x += cellBounds.width - prefSize.width;
		}
		cellBounds.width = prefSize.width;

		return cellBounds.contains(point);
	}

	public static void adjustFocus(JComponent component) {
		if ((!(component.hasFocus())) && (component.isRequestFocusEnabled()))
			component.requestFocus();
	}

	public static int convertModifiersToDropAction(int modifiers, int sourcActions) {
		// PENDING JW: c'p from a decompiled SunDragSourceContextPeer
		// PENDING JW: haha ... completely readable, right ;-)
		int i = 0;

		switch (modifiers & 0xC0) {
			case 192:
				i = 1073741824;
				break;
			case 128:
				i = 1;
				break;
			case 64:
				i = 2;
				break;
			default:
				if ((sourcActions & 0x2) != 0) {
					i = 2;
					break;
				}
				if ((sourcActions & 0x1) != 0) {
					i = 1;
					break;
				}
				if ((sourcActions & 0x40000000) == 0)
					break;
				i = 1073741824;
		}

		// label88:
		return (i & sourcActions);
	}

}
